package com.webex.ta.hydra.util;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.zip.ZipFile;

/**
 * Created by Cisco WebEx.
 * User: vegaz
 * Date: 2010-8-30
 * Time: 14:33:33
 */

public final class ClassFinder {
    private static final String DOT_JAR = ".jar"; // $NON-NLS-1$
    private static final String DOT_CLASS = ".class"; // $NON-NLS-1$
    private static final int DOT_CLASS_LEN = DOT_CLASS.length();

    // static only
	private ClassFinder() {
	}

    /**
     * Filter updates to TreeSet by only storing classes
     * that extend one of the parent classes
     *
     *
     */
    private static class FilterTreeSet extends TreeSet {
        private final Class[] parents; // parent classes to check
        private final boolean inner; // are inner classes OK?

        private final transient ClassLoader contextClassLoader
            = Thread.currentThread().getContextClassLoader(); // Potentially expensive; do it once

        FilterTreeSet(Class []parents, boolean inner){
            super();
            this.parents=parents;
            this.inner=inner;
        }

        /**
         * Override the superclass so we only add classnames that
         * meet the criteria.
         *
         * @param o - classname (must be a String)
         * @return true if it is a new entry
         *
         * @see java.util.TreeSet#add(java.lang.Object)
         */
        public boolean add(Object o){
            if (contains(o)) return false;// No need to check it again
            String s = (String) o;// we only expect Strings
            if ((s.indexOf("$") == -1) || inner) { // $NON-NLS-1$
                if (isChildOf(parents,s, contextClassLoader)) {
                    return super.add(s);
                }
            }
            return false;
        }
    }

    public static List<String> findClassesThatExtend(String path, Class[] superClasses) throws IOException {
        String[] paths = new String[1];
        paths[0] = path;
        return findClassesThatExtend(paths, superClasses);
    }

	public static List<String> findClassesThatExtend(String[] paths, Class[] superClasses)
        throws IOException {
		return findClassesThatExtend(paths, superClasses, false);
	}

    // For each directory in the search path, add all the jars found there
    private static String[] addJarsInPath(String[] paths) {
        Set<String> fullList = new HashSet<String>();
        for (final String path : paths) {
            fullList.add(path); // Keep the unexpanded path
            // TODO - allow directories to end with .jar by removing this check?
            if (!path.endsWith(DOT_JAR)) {
                File dir = new File(path);
                if (dir.exists() && dir.isDirectory()) {
                    String[] jars = dir.list(new FilenameFilter() {
                        public boolean accept(File f, String name) {
                            return name.endsWith(DOT_JAR);
                        }
                    });
                    fullList.addAll(Arrays.asList(jars));
                }
            }
        }
        return fullList.toArray(new String[0]);
    }

	public static List<String> findClassesThatExtend(String[] strPathsOrJars,
            final Class[] superClasses, final boolean innerClasses)
			throws IOException  {
        // Find all jars in the search path
//		strPathsOrJars = addJarsInPath(strPathsOrJars);
        for (int k = 0; k < strPathsOrJars.length; k++) {
            strPathsOrJars[k] = fixPathEntry(strPathsOrJars[k]);
		}

        // Now eliminate any classpath entries that do not "match" the search
//		List listPaths = getClasspathMatches(strPathsOrJars);
        List<String> listPaths = new ArrayList<String>();
        for (String path : strPathsOrJars) {
            listPaths.add(path);
        }

		Set<String> listClasses = new FilterTreeSet(superClasses, innerClasses);
		// first get all the classes
		findClassesInPaths(listPaths, listClasses);


//        // Now keep only the required classes
//		Set subClassList = findAllSubclasses(superClasses, listClasses, innerClasses);
//        if (log.isDebugEnabled()) {
//            log.debug("subClassList.size()="+subClassList.size());
//            Iterator tIter = subClassList.iterator();
//            while (tIter.hasNext()) {
//                log.debug("subClassList : " + tIter.next());
//            }
//        }

		return new ArrayList<String>(listClasses);//subClassList);
	}

    /*
     * Returns the classpath entries that match the search list of jars and paths
     */
	private static List getClasspathMatches(String[] strPathsOrJars) {

        StringTokenizer classPaths =
            new StringTokenizer(System.getProperty("java.class.path"), // $NON-NLS-1$
                System.getProperty("path.separator")); // $NON-NLS-1$

		// find all jar files or paths that end with strPathOrJar
        ArrayList<String> listPaths = new ArrayList<String>();
        String classPath = null;
		while (classPaths.hasMoreTokens()) {
			classPath = fixPathEntry(classPaths.nextToken());
			if (strPathsOrJars == null) {
				listPaths.add(classPath);
			} else {
				boolean found = false;
                for (String strPathsOrJar : strPathsOrJars) {
                    if (classPath.endsWith(strPathsOrJar)) {
                        found = true;
                        listPaths.add(classPath);
                        break;// no need to look further
                    }
                }
				if (!found) {
//					log.debug("Did not find: " + strPath);
				}
			}
		}
		return listPaths;
	}

    /**
     * Fix a path:
     * - replace "." by current directory
     * - trim any trailing spaces
     * - replace \ by /
     * - replace // by /
     * - remove all trailing /
     * @param path
     * @return path
     */
    private static String fixPathEntry(String path){
        if (path == null ) return null;
        if (path.equals(".")) {
            return System.getProperty("user.dir");
        }
        path = path.trim().replace('\\', '/');
        path = substitute(path, "//", "/");

        while (path.endsWith("/")) { // $NON-NLS-1$
            path = path.substring(0, path.length() - 1);
        }
        return path;
    }

	/*
	 * NOTUSED * Determine if the class implements the interface.
	 *
	 * @param theClass
	 *            the class to check
	 * @param theInterface
	 *            the interface to look for
	 * @return boolean true if it implements
	 *
	 * private static boolean classImplementsInterface( Class theClass, Class
	 * theInterface) { HashMap mapInterfaces = new HashMap(); String strKey =
	 * null; // pass in the map by reference since the method is recursive
	 * getAllInterfaces(theClass, mapInterfaces); Iterator iterInterfaces =
	 * mapInterfaces.keySet().iterator(); while (iterInterfaces.hasNext()) {
	 * strKey = (String) iterInterfaces.next(); if (mapInterfaces.get(strKey) ==
	 * theInterface) { return true; } } return false; }
	 */

	/*
	 * Finds all classes that extend the classes in the listSuperClasses
	 * ArrayList, searching in the listAllClasses ArrayList.
	 *
	 * @param superClasses
	 *            the base classes to find subclasses for
	 * @param listAllClasses
	 *            the collection of classes to search in
	 * @param innerClasses
	 *            indicate whether to include inner classes in the search
	 * @return ArrayList of the subclasses
	 */
//	private static Set findAllSubclasses(Class []superClasses, Set listAllClasses, boolean innerClasses) {
//		Set listSubClasses = new TreeSet();
//		for (int i=0; i< superClasses.length; i++) {
//			findAllSubclassesOneClass(superClasses[i], listAllClasses, listSubClasses, innerClasses);
//		}
//		return listSubClasses;
//	}

	/*
	 * Finds all classes that extend the class, searching in the listAllClasses
	 * ArrayList.
	 *
	 * @param theClass
	 *            the parent class
	 * @param listAllClasses
	 *            the collection of classes to search in
	 * @param listSubClasses
	 *            the collection of discovered subclasses
	 * @param innerClasses
	 *            indicates whether inners classes should be included in the
	 *            search
	 */
//	private static void findAllSubclassesOneClass(Class theClass, Set listAllClasses, Set listSubClasses,
//			boolean innerClasses) {
//        Iterator iterClasses = listAllClasses.iterator();
//		while (iterClasses.hasNext()) {
//            String strClassName = (String) iterClasses.next();
//			// only check classes if they are not inner classes
//			// or we intend to check for inner classes
//			if ((strClassName.indexOf("$") == -1) || innerClasses) { // $NON-NLS-1$
//				// might throw an exception, assume this is ignorable
//				try {
//					Class c = Class.forName(strClassName, false, Thread.currentThread().getContextClassLoader());
//
//					if (!c.isInterface() && !Modifier.isAbstract(c.getModifiers())) {
//                        if(theClass.isAssignableFrom(c)){
//                            listSubClasses.add(strClassName);
//                        }
//                    }
//				} catch (Throwable ignored) {
//                    log.debug(ignored.getLocalizedMessage());
//				}
//			}
//		}
//	}

    /**
     *
     * @param parentClasses list of classes to check for
     * @param strClassName name of class to be checked
     * @param contextClassLoader the classloader to use
     * @return
     */
    private static boolean isChildOf(Class [] parentClasses, String strClassName,
            ClassLoader contextClassLoader){
            // might throw an exception, assume this is ignorable
            try {
                Class c = Class.forName(strClassName, false, contextClassLoader);

                if (!c.isInterface() && !Modifier.isAbstract(c.getModifiers())) {
                    for (Class parentClass : parentClasses) {
                        if (parentClass.isAssignableFrom(c)) {
                            return true;
                        }
                    }
                }
            } catch (Throwable ignored) {

            }
        return false;
    }


	/*
	 * Converts a class file from the text stored in a Jar file to a version
	 * that can be used in Class.forName().
	 *
	 * @param strClassName
	 *            the class name from a Jar file
	 * @return String the Java-style dotted version of the name
	 */
	private static String fixClassName(String strClassName) {
		strClassName = strClassName.replace('\\', '.'); // $NON-NLS-1$ // $NON-NLS-2$
		strClassName = strClassName.replace('/', '.'); // $NON-NLS-1$ // $NON-NLS-2$
        // remove ".class"
		strClassName = strClassName.substring(0, strClassName.length() - DOT_CLASS_LEN);
		return strClassName;
	}

	private static void findClassesInOnePath(String strPath, Set<String> listClasses) throws IOException {
        File file = new File(strPath);
		if (file.isDirectory()) {
			findClassesInPathsDir(strPath, file, listClasses);
		} else if (file.exists()) {
            ZipFile zipFile = new ZipFile(file);
            Enumeration entries = zipFile.entries();
			while (entries.hasMoreElements()) {
				String strEntry = entries.nextElement().toString();
				if (strEntry.endsWith(DOT_CLASS)) {
					listClasses.add(fixClassName(strEntry));
				}
			}
            zipFile.close();
		}
	}

	private static void findClassesInPaths(List listPaths, Set listClasses) throws IOException {
        for (Object listPath : listPaths) {
            findClassesInOnePath((String) listPath, listClasses);
        }
	}

	private static void findClassesInPathsDir(String strPathElement, File dir, Set<String> listClasses) throws IOException {
		String[] list = dir.list();
		for (int i = 0; i < list.length; i++) {
            File file = new File(dir, list[i]);
			if (file.isDirectory()) {
                // Recursive call
				findClassesInPathsDir(strPathElement, file, listClasses);
			} else if (list[i].endsWith(DOT_CLASS) && file.exists() && (file.length() != 0)) {
				final String path = file.getPath();
                listClasses.add(path.substring(strPathElement.length() + 1,
                        path.lastIndexOf(".")) // $NON-NLS-1$
						.replace(File.separator.charAt(0), '.')); // $NON-NLS-1$
			} else if (list[i].endsWith(DOT_JAR) && file.exists() && (file.length() != 0)) {
                ZipFile zipFile = new ZipFile(file);
                Enumeration entries = zipFile.entries();
                while (entries.hasMoreElements()) {
                    String strEntry = entries.nextElement().toString();
                    if (strEntry.endsWith(DOT_CLASS)) {
                        listClasses.add(fixClassName(strEntry));
                    }
                }
                zipFile.close();
            }
		}
	}



    public static String substitute(final String input, final String pattern, final String sub) {
        StringBuffer ret = new StringBuffer(input.length());
        int start = 0;
        int index = -1;
        final int length = pattern.length();
        while ((index = input.indexOf(pattern, start)) >= start) {
            ret.append(input.substring(start, index));
            ret.append(sub);
            start = index + length;
        }
        ret.append(input.substring(start));
        return ret.toString();
    }
}